package com.gregbugaj.nscaffold.generator;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.gregbugaj.nscaffold.annotation.RenderingHint;
import com.gregbugaj.nscaffold.annotation.SelectedLabel;
import com.gregbugaj.nscaffold.generator.artifact.Artifact;
import com.gregbugaj.nscaffold.generator.artifact.FileArtifact;
import com.gregbugaj.nscaffold.generator.artifact.FolderArtifact;
import com.gregbugaj.nscaffold.node.PClass;
import com.gregbugaj.nscaffold.node.PropertyInfo;
import com.gregbugaj.nscaffold.type.PType;
import com.gregbugaj.nscaffold.util.ReflectUtil;
import com.gregbugaj.nscaffold.util.TemplateUtil;

/**
 * Default Code generator
 * @author gbugaj
 *
 */
public class CodeGeneratorVisitor implements Visitor, CodeGenerator{
	private List<Artifact> artifacts;
	private File rootDirectory;
	private File webcontentRoot;
	private File packageRoot;
	private File configRoot;
	
	private GenerationContext context;
	private GeneratorState state;

	private Map<String, Object> generationModel = new HashMap<String, Object>();
	private Map<String, Object> indexModel = new HashMap<String, Object>();
	private List<PClass> pclasses=new ArrayList<PClass>();
	private String configManagedBean;
	private String configNavigationRule;
	
	public CodeGeneratorVisitor(GenerationContext context) {
		assert(context!=null) : "GenerationContext can't be null";
		this.context=context;

		artifacts=new ArrayList<Artifact>();

		webcontentRoot=context.getGenerationWebcontentFolder();
		packageRoot=context.getGenerationOutputPackageFolder();
		configRoot=context.getGenerationConfigFolder();

		System.out.println("Webcontent = "+webcontentRoot.getAbsoluteFile());
		System.out.println("Package = "+packageRoot.getAbsoluteFile());
		System.out.println("Config = "+configRoot.getAbsoluteFile());
		
		//aggregated values
		configNavigationRule = "";
		configManagedBean = "";
	}

	@Override
	public void visit(PClass pclass) {
		pclasses.add(pclass);
		
		ArrayList<ArtifactGenerationInfo> artifacts=new ArrayList<ArtifactGenerationInfo>();
		reset();
		// Template data-model
		String name=pclass.getEntityName();
		System.out.println("Enity : "+name);
	

		Artifact artifactWebcontentFolder;
		Artifact artifactPackageFolder;

		Artifact artifactListView;
		Artifact artifactPageView;
		Artifact artifactEditView;
		Artifact artifactListViewBackingBean;
		Artifact artifactViewBackingBean;
		Artifact artifactEditBackingBean;


		//Artifact folder needs to be added first before anything else
		artifactWebcontentFolder = new FolderArtifact();
		artifactWebcontentFolder.setName(pclass.getEntityName());
		artifactWebcontentFolder.setRoot(webcontentRoot);
		addArtifact(artifactWebcontentFolder);

		//Generate the package folder for entity
		artifactPackageFolder = new FolderArtifact();
		artifactPackageFolder.setName(pclass.getEntityName().toLowerCase());
		artifactPackageFolder.setRoot(packageRoot);
		addArtifact(artifactPackageFolder);


		artifactListView = new FileArtifact();
		artifactListView.setName("list.xhtml");
		artifactListView.setRoot(artifactWebcontentFolder.getOutputRoot());
		//Backing bean for list.xhtml
		artifactListViewBackingBean = new FileArtifact();
		artifactListViewBackingBean.setName("List"+buildClassName(pclass)+".java");
		artifactListViewBackingBean.setRoot(artifactPackageFolder.getOutputRoot());



		ArtifactGenerationInfo agList=new ArtifactGenerationInfo();
		agList.artifact=artifactListView;
		agList.template="./models/templates/list.ftl";	
		agList.title=buildBackingBeanName("", pclass)+" listing";
		agList.state=GeneratorState.LIST;
		artifacts.add(agList);



		ArtifactGenerationInfo agListBackingBean=new ArtifactGenerationInfo();
		agListBackingBean.artifact=artifactListViewBackingBean;
		agListBackingBean.template="./models/templates/list_backingbean.ftl";	
		agListBackingBean.state=GeneratorState.LIST_BACKING_BEAN;
		artifacts.add(agListBackingBean);


		//VIEW Page
		artifactPageView = new FileArtifact();
		artifactPageView.setName("view.xhtml");
		artifactPageView.setRoot(artifactWebcontentFolder.getOutputRoot());

		ArtifactGenerationInfo agView=new ArtifactGenerationInfo();
		agView.artifact=artifactPageView;
		agList.title=buildBackingBeanName("", pclass)+" View";
		agView.template="./models/templates/view.ftl";	
		agView.state=GeneratorState.VIEW;

		artifacts.add(agView);

		artifactViewBackingBean = new FileArtifact();
		artifactViewBackingBean.setName("View"+buildClassName(pclass)+".java");
		artifactViewBackingBean.setRoot(artifactPackageFolder.getOutputRoot());

		ArtifactGenerationInfo agViewBackingBean=new ArtifactGenerationInfo();
		agViewBackingBean.artifact=artifactViewBackingBean;
		agViewBackingBean.template="./models/templates/view_backingbean.ftl";	
		agViewBackingBean.state=GeneratorState.VIEW_BACKING_BEAN;
		artifacts.add(agViewBackingBean);


		artifactEditView = new FileArtifact();
		artifactEditView.setName("edit.xhtml");
		artifactEditView.setRoot(artifactWebcontentFolder.getOutputRoot());


		ArtifactGenerationInfo agEdit=new ArtifactGenerationInfo();
		agEdit.artifact=artifactEditView;
		agEdit.title=buildBackingBeanName("", pclass)+" Edit";
		agEdit.template="./models/templates/edit.ftl";	
		agEdit.state=GeneratorState.EDIT;

		artifacts.add(agEdit);

		artifactEditBackingBean = new FileArtifact();
		artifactEditBackingBean.setName("Edit"+buildClassName(pclass)+".java");
		artifactEditBackingBean.setRoot(artifactPackageFolder.getOutputRoot());

		ArtifactGenerationInfo agEditBackingBean=new ArtifactGenerationInfo();
		agEditBackingBean.artifact=artifactEditBackingBean;
		agEditBackingBean.template="./models/templates/edit_backingbean.ftl";	
		agEditBackingBean.state=GeneratorState.EDIT_BACKING_BEAN;
		artifacts.add(agEditBackingBean);


		for(ArtifactGenerationInfo agi: artifacts){
 
			//Common properties for each artifact
			generationModel.put("backingBeanName", buildBackingBeanName("", pclass));
			generationModel.put("entityName", buildBackingBeanName("", pclass));
			generationModel.put("className", buildClassName(pclass));
			generationModel.put("package", context.getOutputPackageName()+"."+buildClassName(pclass).toLowerCase());

			//Setup imports
			generationModel.put("imports", 
					"import "+pclass.getEntityName()+";\n"

			);

			state=agi.state;
			List<PropertyInfo> properties=pclass.getProperties();
			for(PropertyInfo pi:properties){	
				pi.accept(this);
			}

			generationModel.put("welcome", agi.title);
			TemplateUtil.mergeToAsset(agi.artifact, agi.template, generationModel);
			addArtifact(agi.artifact);
			//Reset model at end of each iteration
			generationModel.clear();
		}
		
	}


	private String wrapEL(String string) {
		return String.format("#{%s}", string);
	}

	public String buildBackingBeanName(String prefix, PClass pclass){
		return prefix+buildClassName(pclass);
	}


	public String buildClassName(PClass pclass){
		String name=pclass.getEntityName();
		String[] names=name.split("\\.");
		return names[names.length-1];
	}

	@Override
	public void visit(PropertyInfo property) {
		switch(state){
		case LIST:{
			generatePropertyLIST(property);
			break;
		}

		case VIEW:{
			generatePropertyVIEW(property);
			break;
		}

		case EDIT:{
			generatePropertyEDIT(property);
			break;
		}

		case LIST_BACKING_BEAN:{
			generatePropertyLIST_BACKING_BEAN(property);
			break;
		}
		
		case EDIT_BACKING_BEAN:{
			generatePropertyEDIT_BACKING_BEAN(property);
			break;
		}
		}
	}

	

	/**
	 * Generate necessary config files
	 * @param pclass
	 */
	private void generateCONFIG(PClass pclass) {
		System.out.println("************************************");
		
		String[] filePrefixes=new String[]{"List", "Edit", "View"};
		List<Map<String, String>> listNavRules=new ArrayList<Map<String, String>>();
		List<Map<String, String>> editNavRules=new ArrayList<Map<String, String>>();
		List<Map<String, String>> viewNavRules=new ArrayList<Map<String, String>>();

		String buildBackingBeanName = buildBackingBeanName("", pclass);


		Map<String, String> listFromEdit = new HashMap<String, String>();			
		listFromEdit.put("from", "edit");
		listFromEdit.put("to", "/"+buildBackingBeanName+"/edit.xhtml");

		Map<String, String> listFromView = new HashMap<String, String>();			
		listFromView.put("from", "view");
		listFromView.put("to", "/"+buildBackingBeanName+"/view.xhtml");

		listNavRules.add(listFromEdit);
		listNavRules.add(listFromView);


		Map<String, String> editFromList = new HashMap<String, String>();			
		editFromList.put("from", "list");
		editFromList.put("to", "/"+buildBackingBeanName+"/list.xhtml");

		Map<String, String> editFromCancel = new HashMap<String, String>();			
		editFromCancel.put("from", "cancel");
		editFromCancel.put("to", "/"+buildBackingBeanName+"/list.xhtml");

		editNavRules.add(editFromList);
		editNavRules.add(editFromCancel);


		List<List<Map<String, String>>>  navigations=new ArrayList<List<Map<String, String>>>();
		navigations.add(listNavRules);
		navigations.add(editNavRules);
		navigations.add(viewNavRules);

		for(int i=0;i<3;i++){
			String prefix =  filePrefixes[i];

			Map<String, Object> managedBeanModel=new HashMap<String, Object>();
			managedBeanModel.put("name", buildBackingBeanName(prefix.toLowerCase(), pclass));		
			managedBeanModel.put("class",  context.getOutputPackageName()+"."+buildClassName(pclass).toLowerCase()+"."+buildBackingBeanName(prefix, pclass));
			managedBeanModel.put("scope", "request");	

			configManagedBean+=TemplateUtil.merge("./models/config/jsf/managed-been-part.ftl", managedBeanModel);
			configManagedBean+="\n\n";


			//Navigation Rules
			Map<String, Object> navigationRuleModel=new HashMap<String, Object>();
			navigationRuleModel.put("fromViewId","/"+buildBackingBeanName+"/"+prefix.toLowerCase()+".xhtml");		
			navigationRuleModel.put("navigations", navigations.get(i));	


			configNavigationRule+=TemplateUtil.merge("./models/config/jsf/navigation-rule-part.ftl", navigationRuleModel);
			configNavigationRule+="\n\n";
		}

	
	}

	/**
	 * Called when State is EDIT
	 * @param property
	 */
	private void generatePropertyEDIT(PropertyInfo property) {
		String editModel=getRenderingHintModel(property, false);
		//Check if we have already created entities entry in the map
		@SuppressWarnings("unchecked")
		List<Map<String, Object>> entities=(List<Map<String, Object>>) generationModel.get("entities");
		if(entities==null){
			entities=new ArrayList<Map<String, Object>>();
		}

		Map<String, Object> model=new HashMap<String, Object>();
		model.putAll(generationModel);

		model.put("value", wrapEL(buildIterableVariable(property)));	
		model.put("var",  buildIterableVariableName(property));
		model.put("name", property.getPropertyName());	

		
		Map<String, Object> entity = new  HashMap<String, Object>();
		entity.put("name", property.getPropertyName());
		entity.put("iscollection", property.isCollection());

		entity.put("render", TemplateUtil.merge(editModel, model));
		entities.add(entity);

		generationModel.put("entities", entities);
	}



	private void generatePropertyLIST_BACKING_BEAN(PropertyInfo property) {
		generatePropertyLIST(property);
	}
	
	private void generatePropertyEDIT_BACKING_BEAN(PropertyInfo property) {
		generatePropertyLIST(property);
	}

	/**
	 * Currently view and list render properties this same way
	 * @param property
	 */
	private void generatePropertyVIEW(PropertyInfo property) {
		generatePropertyLIST(property);
	}


	private void generatePropertyLIST(PropertyInfo property) {	
		String viewModel=getRenderingHintModel(property, true);
		//Check if we have already created entities entry in the map
		@SuppressWarnings("unchecked")
		List<Map<String, Object>> entities=(List<Map<String, Object>>) generationModel.get("entities");
		if(entities==null){
			entities=new ArrayList<Map<String, Object>>();
		}

		Map<String, Object> model=new HashMap<String, Object>();
		model.put("value", wrapEL( buildIterableVariable(property) ));
		model.put("var",  buildIterableVariableName(property));
		model.put("hasLabel",  false);	

		//Represent simple entiry from current property
		Map<String, Object> entity = new  HashMap<String, Object>();
		entity.put("name", property.getPropertyName());
		entity.put("iscollection", property.isCollection());
		
		if(PType.OneToOneType.equals(property.getType())
				|| PType.CollectionType.equals(property.getType())
				|| PType.ManyToOneType.equals(property.getType())		
		){
			entity.put("iscollection", true);
			
			//Check if SelectedLabel annotation is present on the Extension Class of this property
			Class<?> propertyEntityClass=property.getPropertyClass();
			Class<?> propertyExtensionClass = ReflectUtil.getExtensionClass(propertyEntityClass, context.getEntensisonPackageName());
			if(ReflectUtil.hasMethodAnnotation(propertyExtensionClass, SelectedLabel.class)){
				model.put("hasLabel",  true);	
				model.put("label",  ReflectUtil.getBeanName(ReflectUtil.getAnnotatedMethod(propertyExtensionClass, SelectedLabel.class)));	
			}
		}

	
		entity.put("render", TemplateUtil.merge(viewModel, model));
		entities.add(entity);
		generationModel.put("entities", entities);
	}


	/**
	 * Get the Template Model associated with given property
	 * @param property
	 * @param useViewModel
	 * @return
	 */
	private String getRenderingHintModel(PropertyInfo property, boolean useViewModel) {
		Class<?> entityClass=property.getParent().getEntityClass();
		PType type=property.getType();
		String model="";
		if(useViewModel){
			model=type.getViewModel();
		}else{
			model=type.getEditModel();
		}

		//Check for rendering hints for this property
		Class<?> extensionClass = ReflectUtil.getExtensionClass(entityClass, context.getEntensisonPackageName());
		if(extensionClass!=null && ReflectUtil.hasRenderingHint(extensionClass, property.getPropertyName())){
			RenderingHint renderingHint = ReflectUtil.getRenderingHint(extensionClass, property.getPropertyName());
			if(useViewModel){
				if(com.gregbugaj.nscaffold.util.StringUtil.isNullOrEmpty(renderingHint.viewModel()) == false){
					model=renderingHint.viewModel();	
				}				
			}else{
				if(com.gregbugaj.nscaffold.util.StringUtil.isNullOrEmpty(renderingHint.editModel()) == false){
					model=renderingHint.editModel();	
				}
			}
		}
		return model;
	}

	/**
	 * Build name 
	 * 
	 * @param property
	 * @return
	 */
	public String buildIterableVariable(PropertyInfo property) {
		String name="";
		String entityName="item";			
		name=String.format("%s.%s", entityName, property.getPropertyName());

		Class<?> entityClass=property.getParent().getEntityClass();
		Class<?> extensionClass = ReflectUtil.getExtensionClass(entityClass, context.getEntensisonPackageName());

		if(PType.OneToOneType.equals(property.getType())
				|| PType.CollectionType.equals(property.getType())
				|| PType.ManyToOneType.equals(property.getType())		
		){
			if(ReflectUtil.hasMethodAnnotation(extensionClass, SelectedLabel.class)){
				String label=ReflectUtil.getBeanName(ReflectUtil.getAnnotatedMethod(extensionClass, SelectedLabel.class));
				name=name+"."+label;
			}
		}

		return name;
	}

	/**
	 * Create variable name to be used for loops, foreach etc..
	 * @param property
	 * @return
	 */
	public String buildIterableVariableName(PropertyInfo property) {
		String prefix="item";	
		String sufix=property.getPropertyName();
		sufix=sufix.
		substring(0, 1).toUpperCase() +
		sufix.substring(1, sufix.length());

		//Remove sufix s from property name
		if(sufix.endsWith("s")){
			sufix=sufix.substring(0, sufix.length()-1);	
		}
		return String.format("%s%s",prefix, sufix);
	}


	@Override
	public void addArtifact(Artifact artifact) {
		artifacts.add(artifact);
	}


	@Override
	public List<Artifact> getArtifacts() {
		return artifacts;
	}


	@Override
	public void generate() {
		assert(rootDirectory != null) : "rootDirectory can't be null";

		for(PClass pc:pclasses){
			generateCONFIG(pc);
			generateINDEX(pc);	
		}
		
		//Generate Index
		Artifact artifactIndex = new FileArtifact();
		artifactIndex.setName("index.xhtml");
		artifactIndex.setRoot(webcontentRoot);
		TemplateUtil.mergeToAsset(artifactIndex, "./models/templates/index.ftl", indexModel);		
		addArtifact(artifactIndex);
		
		
		//Generate configuration
		Artifact artifactConfigFile = new FileArtifact();
		artifactConfigFile.setName("faces-config-nsf.xml");
		artifactConfigFile.setRoot(configRoot);
		Map<String, Object> model=new HashMap<String, Object>();
		model.put("managedbean", configManagedBean);
		model.put("navigationrule", configNavigationRule);
		TemplateUtil.mergeToAsset(artifactConfigFile, "./models/config/jsf/faces-config.ftl", model);		
		addArtifact(artifactConfigFile);
		
		//Render all the artifact to disk
		for(Artifact artifact:getArtifacts()){
			artifact.render();
		}
	}

	/**
	 * Generate index page
	 * @param pc
	 */
	private void generateINDEX(PClass pclass) {
		@SuppressWarnings("unchecked")
		List<Map<String, String>> entities=(List<Map<String, String>>) indexModel.get("entities");
		if(entities==null){
			entities=new ArrayList<Map<String,String>>();
		}
		Map<String, String> entity = new  HashMap<String, String>();
		entity.put("name", buildClassName(pclass));
		entity.put("url","./"+buildClassName(pclass)+"/list.jsf");
		entities.add(entity);
		indexModel.put("entities", entities);		
	}

	@Override
	public void reset() {
		generationModel=new HashMap<String, Object>();
	}


	private enum GeneratorState{
		VIEW,
		LIST,
		EDIT, 

		LIST_BACKING_BEAN, 
		VIEW_BACKING_BEAN, 
		EDIT_BACKING_BEAN, 
	};

	private class ArtifactGenerationInfo{
		Artifact artifact;
		String template;
		String title;
		GeneratorState state;
	}
}
